001 /*
002 * Copyright (c) 2005 Stephen J. McConnell
003 *
004 * Licensed under the Apache License, Version 2.0 (the "License");
005 * you may not use this file except in compliance with the License.
006 * You may obtain a copy of the License at
007 *
008 * http://www.apache.org/licenses/LICENSE-2.0
009 *
010 * Unless required by applicable law or agreed to in writing, software
011 * distributed under the License is distributed on an "AS IS" BASIS,
012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
013 * implied.
014 *
015 * See the License for the specific language governing permissions and
016 * limitations under the License.
017 */
018
019 package net.dpml.metro.tools;
020
021 import java.beans.IntrospectionException;
022 import java.io.IOException;
023 import java.io.InputStream;
024 import java.io.File;
025 import java.io.FileOutputStream;
026 import java.io.OutputStream;
027 import java.lang.reflect.Method;
028 import java.net.URI;
029 import java.net.URL;
030 import java.net.URISyntaxException;
031 import java.util.ArrayList;
032 import java.util.List;
033 import java.util.Map;
034 import java.util.Properties;
035
036 import net.dpml.metro.builder.ComponentTypeEncoder;
037
038 import net.dpml.metro.info.CategoryDescriptor;
039 import net.dpml.metro.info.ContextDescriptor;
040 import net.dpml.metro.info.InfoDescriptor;
041 import net.dpml.metro.info.LifestylePolicy;
042 import net.dpml.metro.info.CollectionPolicy;
043 import net.dpml.metro.info.Type;
044 import net.dpml.metro.info.EntryDescriptor;
045 import net.dpml.metro.info.ServiceDescriptor;
046 import net.dpml.metro.info.ThreadSafePolicy;
047
048 import net.dpml.state.State;
049 import net.dpml.state.DefaultState;
050 import net.dpml.state.StateDecoder;
051
052 import net.dpml.library.info.Scope;
053
054 import net.dpml.tools.tasks.GenericTask;
055
056 import org.apache.tools.ant.BuildException;
057 import org.apache.tools.ant.Project;
058 import org.apache.tools.ant.AntClassLoader;
059 import org.apache.tools.ant.types.Path;
060
061
062 /**
063 * The TypeTask creates a serialized descriptor of a component type.
064 *
065 * @author <a href="http://www.dpml.net">Digital Product Meta Library</a>
066 * @version 1.0.0
067 */
068 public class TypeBuilderTask extends GenericTask implements TypeBuilder
069 {
070 private static final StateDecoder STATE_DECODER = new StateDecoder();
071 private static final ComponentTypeEncoder COMPONENT_TYPE_ENCODER =
072 new ComponentTypeEncoder();
073
074 //---------------------------------------------------------------
075 // state
076 //---------------------------------------------------------------
077
078 private String m_name;
079 private String m_classname;
080 private Class m_class;
081 private LifestylePolicy m_lifestyle;
082 private CollectionPolicy m_collection;
083 private ThreadSafePolicy m_threadsafe = ThreadSafePolicy.UNKNOWN;
084 private StateDataType m_state;
085 private ServicesDataType m_services;
086 private CategoriesDescriptorDataType m_categories;
087
088 //---------------------------------------------------------------
089 // setters
090 //---------------------------------------------------------------
091
092 /**
093 * Set the name of the type.
094 * @param name the component name
095 */
096 public void setName( String name )
097 {
098 m_name = name;
099 }
100
101 /**
102 * Set the classname of the type.
103 * @param classname the component type classname
104 */
105 public void setClass( String classname )
106 {
107 m_classname = classname;
108 }
109
110 /**
111 * Set the threadsafe flag.
112 * @param flag true if the component type is threadsafe
113 */
114 public void setThreadsafe( boolean flag )
115 {
116 if( flag )
117 {
118 m_threadsafe = ThreadSafePolicy.TRUE;
119 }
120 else
121 {
122 m_threadsafe = ThreadSafePolicy.FALSE;
123 }
124 }
125
126 /**
127 * Set the type collection policy.
128 * @param value the collection policy value
129 */
130 public void setCollection( String value )
131 {
132 m_collection = CollectionPolicy.parse( value );
133 }
134
135 /**
136 * Set the type lifestyle policy.
137 * @param value the lifestyle policy value
138 */
139 public void setLifestyle( String value )
140 {
141 m_lifestyle = LifestylePolicy.parse( value );
142 }
143
144 /**
145 * Create a new services datatype.
146 * @return a new services datatype
147 */
148 public ServicesDataType createServices()
149 {
150 if( m_services == null )
151 {
152 m_services = new ServicesDataType();
153 return m_services;
154 }
155 else
156 {
157 final String error =
158 "Illegal attempt to create a duplicate services element.";
159 throw new BuildException( error, getLocation() );
160 }
161 }
162
163 /**
164 * Create a new services datatype.
165 * @return a new services datatype
166 */
167 public CategoriesDescriptorDataType createCategories()
168 {
169 if( m_categories == null )
170 {
171 m_categories = new CategoriesDescriptorDataType();
172 return m_categories;
173 }
174 else
175 {
176 final String error =
177 "Illegal attempt to create a duplicate categories element.";
178 throw new BuildException( error, getLocation() );
179 }
180 }
181
182 /**
183 * Create a state descriptor for the component.
184 * @return a state graph descriptor
185 */
186 public StateDataType createState()
187 {
188 if( m_state == null )
189 {
190 m_state = new StateDataType( this, true );
191 return m_state;
192 }
193 else
194 {
195 final String error =
196 "Illegal attempt to create a duplicate state element.";
197 throw new BuildException( error, getLocation() );
198 }
199 }
200
201 //---------------------------------------------------------------
202 // Builder
203 //---------------------------------------------------------------
204
205 /**
206 * Return a urn identitifying the component type strategy that this
207 * builder is supporting.
208 *
209 * @return a uri identifiying the type strategy
210 */
211 public URI getTypeHandlerURI()
212 {
213 return TYPE_HANDLER_URI;
214 }
215
216 /**
217 * Return a uri identitifying the builder.
218 *
219 * @return a uri identifiying the type builder
220 */
221 public URI getBuilderURI()
222 {
223 return COMPONENT_TYPE_DECODER_URI;
224 }
225
226 //---------------------------------------------------------------
227 // TypeBuilder
228 //---------------------------------------------------------------
229
230 /**
231 * Build the type.
232 * @param classloader the classloader to use for type creation
233 * @return the component type
234 * @exception IntrospectionException if a class introspection error occurs
235 * @exception IOException if an I/O error occurs
236 */
237 public Type buildType( ClassLoader classloader )
238 throws IntrospectionException, IOException
239 {
240 Class subject = loadSubjectClass( classloader );
241 return buildType( subject );
242 }
243
244 /**
245 * Build the type.
246 * @param subject the implementation class
247 * @return the component type
248 * @exception IntrospectionException if a class introspection error occurs
249 * @exception IOException if an I/O error occurs
250 */
251 public Type buildType( Class subject )
252 throws IntrospectionException, IOException
253 {
254 log( "creating [" + subject.getName() + "]" );
255
256 InfoDescriptor info = createInfoDescriptor( subject );
257 ServiceDescriptor[] services = createServiceDescriptors( subject );
258 CategoryDescriptor[] categories = createCategoryDescriptors();
259 ContextDescriptor context = createContextDescriptor( subject );
260 State graph = getStateGraph( subject );
261 return new Type( info, categories, context, services, graph );
262 }
263
264 private File getReportDestination( File dir, Type type )
265 {
266 final String classname = type.getInfo().getClassname();
267 String path = classname.replace( '.', '/' );
268 String filename = path + ".xml";
269 return new File( dir, filename );
270 }
271
272 //---------------------------------------------------------------
273 // Task
274 //---------------------------------------------------------------
275
276 /**
277 * Execute the task.
278 */
279 public void execute()
280 {
281 Project proj = getProject();
282
283 Path path = getContext().getPath( Scope.RUNTIME );
284 File classes = getContext().getTargetClassesMainDirectory();
285 path.createPathElement().setLocation( classes );
286 ClassLoader classloader = new AntClassLoader( proj, path );
287 ClassLoader current = Thread.currentThread().getContextClassLoader();
288 try
289 {
290 final Type type = buildType( classloader );
291 OutputStream output = getOutputStream( type );
292 try
293 {
294 COMPONENT_TYPE_ENCODER.export( type, output );
295 }
296 finally
297 {
298 try
299 {
300 output.close();
301 }
302 catch( IOException ioe )
303 {
304 ioe.printStackTrace();
305 }
306 }
307 }
308 catch( IntrospectionException e )
309 {
310 final String error = e.getMessage();
311 throw new BuildException( error, e, getLocation() );
312 }
313 catch( BuildException e )
314 {
315 throw e;
316 }
317 catch( Throwable e )
318 {
319 final String error =
320 "Internal error while attempting to build types."
321 + "\nCause: " + e.getClass().getName()
322 + "\nMessage: " + e.getMessage();
323 throw new BuildException( error, e, getLocation() );
324 }
325 }
326
327 private OutputStream getOutputStream( Type type ) throws IOException
328 {
329 final String classname = type.getInfo().getClassname();
330 final String resource = getEmbeddedResourcePath( classname );
331 final File file = getEmbeddedOutputFile( resource );
332 file.getParentFile().mkdirs();
333 return new FileOutputStream( file );
334 }
335
336 private String getEmbeddedResourcePath( String classname )
337 {
338 String path = classname.replace( '.', '/' );
339 String filename = path + ".type";
340 return filename;
341 }
342
343 private File getEmbeddedOutputFile( String filename )
344 {
345 File classes = getContext().getTargetClassesMainDirectory();
346 File destination = new File( classes, filename );
347 return destination;
348 }
349
350 //---------------------------------------------------------------
351 // internals
352 //---------------------------------------------------------------
353
354 /**
355 * Return the type name.
356 * @return the name
357 */
358 protected String getName()
359 {
360 if( null == m_name )
361 {
362 return "untitled";
363 }
364 return m_name;
365 }
366
367 /**
368 * Return the type classname.
369 * @return the classname
370 */
371 protected String getClassname()
372 {
373 if( null == m_classname )
374 {
375 final String error =
376 "Component type does not declare a classname.";
377 throw new BuildException( error, getLocation() );
378 }
379 return m_classname;
380 }
381
382 private InfoDescriptor createInfoDescriptor( Class subject )
383 throws IntrospectionException
384 {
385 String name = getName();
386 String classname = subject.getName();
387 ThreadSafePolicy threadsafe = getThreadSafeCapability( subject );
388 Properties properties = getTypeProperties( subject );
389 return new InfoDescriptor(
390 name, classname, null, m_lifestyle, m_collection, threadsafe, properties );
391 }
392
393 private ThreadSafePolicy getThreadSafeCapability( Class subject )
394 throws IntrospectionException
395 {
396 return m_threadsafe;
397 }
398
399 private Properties getTypeProperties( Class subject )
400 throws IntrospectionException
401 {
402 String path = subject.getClass().getName().replace( '.', '/' ) + ".properties";
403 URL url = subject.getResource( path );
404 if( null == url )
405 {
406 return null;
407 }
408 else
409 {
410 try
411 {
412 Properties properties = new Properties();
413 InputStream input = url.openStream();
414 try
415 {
416 properties.load( input );
417 return properties;
418 }
419 finally
420 {
421 input.close();
422 }
423 }
424 catch( IOException e )
425 {
426 final String error =
427 "Unable to load the property file for the path: "
428 + path;
429 throw new BuildException( error, e );
430 }
431 }
432 }
433
434 private ServiceDescriptor[] createServiceDescriptors( Class subject )
435 {
436 if( null == m_services )
437 {
438 ArrayList list = new ArrayList();
439 return createServiceDescriptors( subject, list );
440 }
441 else
442 {
443 return m_services.getServiceDescriptors();
444 }
445 }
446
447 private CategoryDescriptor[] createCategoryDescriptors()
448 {
449 if( null == m_categories )
450 {
451 return new CategoryDescriptor[0];
452 }
453 else
454 {
455 return m_categories.getCategoryDescriptors();
456 }
457 }
458
459 private ServiceDescriptor[] createServiceDescriptors( Class subject, List list )
460 {
461 Class[] interfaces = subject.getInterfaces();
462 for( int i=0; i<interfaces.length; i++ )
463 {
464 Class service = interfaces[i];
465 ServiceDescriptor descriptor = createServiceDescriptor( subject, service );
466 if( null != descriptor )
467 {
468 if( !list.contains( descriptor ) )
469 {
470 list.add( descriptor );
471 }
472 }
473 }
474 Class superclass = subject.getSuperclass();
475 if( null != superclass )
476 {
477 return createServiceDescriptors( superclass, list );
478 }
479 else
480 {
481 return (ServiceDescriptor[]) list.toArray( new ServiceDescriptor[0] );
482 }
483 }
484
485 private ServiceDescriptor createServiceDescriptor( Class type, Class subject )
486 {
487 String classname = subject.getName();
488 Class parent = subject.getDeclaringClass();
489 if( classname.startsWith( "java." ) )
490 {
491 return null; // ignore java.* interfaces
492 }
493 else if( classname.startsWith( "net.dpml.activity." ) )
494 {
495 return null;
496 }
497 else if( type == parent )
498 {
499 return null; // ignore immediate inner interfaces
500 }
501 else
502 {
503 return new ServiceDescriptor( classname );
504 }
505 }
506
507 private ContextDescriptor createContextDescriptor( Class subject )
508 throws IntrospectionException
509 {
510 EntryDescriptor[] entries = createEntryDescriptors( subject );
511 return new ContextDescriptor( entries );
512 }
513
514 private EntryDescriptor[] createEntryDescriptors( Class subject )
515 throws IntrospectionException
516 {
517 String classname = subject.getName();
518 Class[] classes = subject.getClasses();
519 Class param = locateClass( "$Context", classes );
520 if( null == param )
521 {
522 return new EntryDescriptor[0];
523 }
524 else
525 {
526 //
527 // For each method in the Context inner-interface we construct a
528 // descriptor that establishes the part key, type, and required status.
529 //
530
531 Method[] methods = param.getMethods();
532 ArrayList list = new ArrayList();
533 for( int i=0; i<methods.length; i++ )
534 {
535 Method method = methods[i];
536 String name = method.getName();
537 if( name.startsWith( "get" ) )
538 {
539 EntryDescriptor descriptor = createEntryDescriptor( method );
540 list.add( descriptor );
541 }
542 }
543 return (EntryDescriptor[]) list.toArray( new EntryDescriptor[0] );
544 }
545 }
546
547 /**
548 * Creation of a new parameter descriptor using a supplied method.
549 * The method is the method used by the component implementation to get the parameter
550 * instance.
551 */
552 private EntryDescriptor createEntryDescriptor( Method method )
553 throws IntrospectionException
554 {
555 validateMethodName( method );
556 validateNoExceptions( method );
557
558 String key = EntryDescriptor.getEntryKey( method );
559
560 Class returnType = method.getReturnType();
561 if( method.getParameterTypes().length == 0 )
562 {
563 //
564 // required context entry
565 //
566
567 validateNonNullReturnType( method );
568 //validateNonArrayReturnType( method, returnType );
569 String type = returnType.getName();
570 return new EntryDescriptor( key, type, EntryDescriptor.REQUIRED );
571 }
572 else if( method.getParameterTypes().length == 1 )
573 {
574 Class[] params = method.getParameterTypes();
575 Class param = params[0];
576 if( returnType.isAssignableFrom( param ) )
577 {
578 String type = param.getName();
579 return new EntryDescriptor( key, type, EntryDescriptor.OPTIONAL );
580 }
581 else
582 {
583 final String error =
584 "Context entry assessor declares an optional default parameter class ["
585 + param.getName()
586 + "] which is not assignable to the return type ["
587 + returnType.getName()
588 + "]";
589 throw new IntrospectionException( error );
590 }
591 }
592 else
593 {
594 final String error =
595 "Unable to establish a required or optional context entry method pattern on ["
596 + method.getName()
597 + "]";
598 throw new IntrospectionException( error );
599 }
600 }
601
602 private State getStateGraph( Class subject )
603 {
604 if( null == m_state )
605 {
606 return new DefaultState( "" );
607 }
608 else
609 {
610 return m_state.getState();
611 }
612 }
613
614 private State loadStateFromResource( Class subject )
615 {
616 String resource = subject.getName().replace( '.', '/' ) + ".xgraph";
617 try
618 {
619 URL url = subject.getClassLoader().getResource( resource );
620 if( null == url )
621 {
622 return null;
623 }
624 else
625 {
626 URI uri = new URI( url.toString() );
627 return STATE_DECODER.loadState( uri );
628 }
629 }
630 catch( Throwable e )
631 {
632 final String error =
633 "Internal error while attempting to load component state graph resource ["
634 + resource
635 + "].";
636 throw new BuildException( error, e );
637 }
638 }
639
640 private String formatKey( String key, int offset )
641 {
642 String k = key.substring( offset );
643 return formatKey( k );
644 }
645
646 private String formatKey( String key )
647 {
648 if( key.length() < 1 )
649 {
650 throw new IllegalArgumentException( "key" );
651 }
652 String first = key.substring( 0, 1 ).toLowerCase();
653 String remainder = key.substring( 1 );
654 return first + remainder;
655 }
656
657 private Class locateClass( String postfix, Class[] classes )
658 {
659 for( int i=0; i<classes.length; i++ )
660 {
661 Class inner = classes[i];
662 String name = inner.getName();
663 if( name.endsWith( postfix ) )
664 {
665 return inner;
666 }
667 }
668 return null;
669 }
670
671 private void validateMapReturnType( Method method )
672 throws IntrospectionException
673 {
674 Class returnType = method.getReturnType();
675 if( Map.class != returnType )
676 {
677 String name = method.getName();
678 final String error =
679 "The method ["
680 + name
681 + "] does not declare java.util.Map as a return type.";
682 throw new IntrospectionException( error );
683 }
684 }
685
686 private void validateEntryReturnType( Method method )
687 throws IntrospectionException
688 {
689 Class returnType = method.getReturnType();
690 if( Map.Entry.class != returnType )
691 {
692 String name = method.getName();
693 final String error =
694 "The method ["
695 + name
696 + "] does not declare java.util.Map.Entry as a return type.";
697 throw new IntrospectionException( error );
698 }
699 }
700
701 private void validateReturnTypeIsAssignable( Method method, Class type )
702 throws IntrospectionException
703 {
704 Class c = method.getReturnType();
705 if( !type.isAssignableFrom( c ) )
706 {
707 final String error =
708 "Method ["
709 + method.getName()
710 + "] declares a return type ["
711 + c.getName()
712 + "] that is not assignable from the class ["
713 + type.getName()
714 + "].";
715 throw new IntrospectionException( error );
716 }
717 }
718
719 private void validateNonNullParameter( Method method, Class type )
720 throws IntrospectionException
721 {
722 if( Void.TYPE.equals( type ) )
723 {
724 final String error =
725 "Method ["
726 + method.getName()
727 + "] declares a null parameter.";
728 throw new IntrospectionException( error );
729 }
730 }
731
732
733 private void validateNonNullReturnType( Method method )
734 throws IntrospectionException
735 {
736 Class returnType = method.getReturnType();
737 if( Void.TYPE.equals( returnType ) )
738 {
739 final String error =
740 "Method ["
741 + method.getName()
742 + "] does not declare a return type.";
743 throw new IntrospectionException( error );
744 }
745 }
746
747 private void validateNullReturnType( Method method, Class returnType )
748 throws IntrospectionException
749 {
750 if( !Void.TYPE.equals( returnType ) )
751 {
752 final String error =
753 "Method ["
754 + method.getName()
755 + "] does not declare a null return type.";
756 throw new IntrospectionException( error );
757 }
758 }
759
760 private void validateNonArrayReturnType( Method method, Class returnType )
761 throws IntrospectionException
762 {
763 if( null != returnType.getComponentType() )
764 {
765 final String error =
766 "Method ["
767 + method.getName()
768 + "] declares an array return type.";
769 throw new IntrospectionException( error );
770 }
771 }
772
773 private void validateNonArrayType( Method method, Class type )
774 throws IntrospectionException
775 {
776 if( null != type.getComponentType() )
777 {
778 final String error =
779 "Method ["
780 + method.getName()
781 + "] declares an array type.";
782 throw new IntrospectionException( error );
783 }
784 }
785
786 private void validateInterfaceReturnType( Method method, Class returnType )
787 throws IntrospectionException
788 {
789 if( !returnType.isInterface() )
790 {
791 final String error =
792 "Method ["
793 + method.getName()
794 + "] declares a return type ["
795 + returnType.getName()
796 + "] that is not an interface.";
797 throw new IntrospectionException( error );
798 }
799 }
800
801 private void validateMethodName( Method method )
802 throws IntrospectionException
803 {
804 if( !method.getName().startsWith( "get" ) )
805 {
806 final String error =
807 "Method ["
808 + method.getName()
809 + "] does not start with 'get'.";
810 throw new IntrospectionException( error );
811 }
812 }
813
814 private void validateNoExceptions( Method method )
815 throws IntrospectionException
816 {
817 Class[] exceptionTypes = method.getExceptionTypes();
818 if( exceptionTypes.length > 0 )
819 {
820 final String error =
821 "Method ["
822 + method.getName()
823 + "] declares one or more exceptions.";
824 throw new IntrospectionException( error );
825 }
826 }
827
828 private void validateNoParameters( Method method )
829 throws IntrospectionException
830 {
831 Class[] parameterTypes = method.getParameterTypes();
832 if( parameterTypes.length > 0 )
833 {
834 final String error =
835 "Method ["
836 + method.getName()
837 + "] declares one or more parameters.";
838 throw new IntrospectionException( error );
839 }
840 }
841
842 private void validateAtMostOneParameter( Method method )
843 throws IntrospectionException
844 {
845 Class[] parameterTypes = method.getParameterTypes();
846 if( parameterTypes.length > 1 )
847 {
848 final String error =
849 "Method ["
850 + method.getName()
851 + "] declares more than one parameters.";
852 throw new IntrospectionException( error );
853 }
854 }
855
856 private Class validateSingleParameter( Method method )
857 throws IntrospectionException
858 {
859 Class[] parameterTypes = method.getParameterTypes();
860 if( parameterTypes.length != 1 )
861 {
862 final String error =
863 "Method ["
864 + method.getName()
865 + "] does not declare a single parameter argument type.";
866 throw new IntrospectionException( error );
867 }
868 return parameterTypes[0];
869 }
870
871 private void validateNonArrayParameter( Method method, Class type )
872 throws IntrospectionException
873 {
874 if( null != type.getComponentType() )
875 {
876 final String error =
877 "Method ["
878 + method.getName()
879 + "] declares an array parameter type.";
880 throw new IntrospectionException( error );
881 }
882 }
883
884 private void validateSelectPattern( Class subject, Method method )
885 throws IntrospectionException
886 {
887 Class[] parameterTypes = method.getParameterTypes();
888 int n = parameterTypes.length;
889 if( n == 1 )
890 {
891 Class b = parameterTypes[0];
892 if( !Boolean.TYPE.isAssignableFrom( b ) )
893 {
894 String name = method.getName();
895 final String error =
896 "Part accessor ["
897 + subject.getName() + "#" + name
898 + "] is declaring an illegal non-boolean parameter.";
899 throw new IntrospectionException( error );
900 }
901 }
902 }
903
904 private Class loadSubjectClass( ClassLoader classloader )
905 {
906 if( null == m_classname )
907 {
908 final String error =
909 "Missing component descriptor class attribute.";
910 throw new IllegalStateException( error );
911 }
912 try
913 {
914 return classloader.loadClass( m_classname );
915 }
916 catch( ClassNotFoundException e )
917 {
918 final String error =
919 "Cannot build a component type because the class ["
920 + m_classname
921 + "] is not present in the project path.";
922 throw new BuildException( error );
923 }
924 }
925
926 private static final URI TYPE_HANDLER_URI = setupURI( "artifact:part:dpml/metro/dpml-metro-runtime#1.0.0" );
927 private static final URI COMPONENT_TYPE_DECODER_URI = setupURI( "artifact:part:dpml/metro/dpml-metro-tools#1.0.0" );
928
929 /**
930 * Internal utility to create a static uri.
931 * @param spec the uri spec
932 * @return the uri
933 */
934 protected static URI setupURI( String spec )
935 {
936 try
937 {
938 return new URI( spec );
939 }
940 catch( URISyntaxException ioe )
941 {
942 return null;
943 }
944 }
945 }